import dataclasses
import datetime
import logging
import re
import threading
from typing import Any, Callable, List, Optional

import pydash
import ujson as json

from ys_python.common.base.json_util import JSONUtil
from ys_python.common.base.logger import get_formatter, get_app_logger
from ys_python.common.mq.mq import GRPCMQ
from ys_python.common.mq.ys_queue import IYSQueue


@dataclasses.dataclass
class MQModuleCfg:
    """
    MQHandler 的配置参数

    Attributes:
        name : 模块名
        useHeartHandler: 是否使用默认的心跳函数

    """
    name: str = ""  # 配置名称
    useHeartHandler: bool = False  # 是否启动
    hasSubAllTopic: bool = False  # 是否订阅所有话题
    useLoggerHandler: bool = False  # 是否使用 mq 发送logger
    useUnPack: bool = False  # 使用自动化组包功能
    useConfigCenter: bool = False  # 是否使用配置中心


def defult_MQModuleCfg() -> MQModuleCfg:
    return MQModuleCfg(useHeartHandler=True, useUnPack=True, useConfigCenter=True)


@dataclasses.dataclass
class MQModuleHandlerFunc:
    """
    MQHandler topic 处理函数结构

    Attributes:
        topic : 需要处理的 topic, 使用前缀匹配模式
        func: 处理函数，第一个参数是 topic，第二个参数是 msg
    """

    topic: str = ""

    # 处理函数 第一个参数是 topic 第二个参数是 msg
    func: Callable = lambda topic, msg: print(topic)


class MQTopic:
    """MQ话题工具类

    - 为生成 MQ 话题提供支持；
    - 设置好 name 后可以快速生成不同 model 的必要话题

    Attributes:
        HEART: 心跳话题
        HEART_ASK: 心跳询问话题
        REQ: 请求话题
        RES: 响应话题
        DATA: 数据话题
        DATA_RAW: 原始数据话题
        DATA_LTTB: lttb 算法压缩的话题
        CFG: 配置话题
        CFG_ASK: 配置询问话题
        LOG: 日志话题
    """
    HEART = "json.heart"
    HEART_ASK = "".join([HEART, "?"])

    REQ = "json.req"
    RES = "json.res"

    DATA = "json.data"
    DATA_RAW = "json.data.raw"
    DATA_LTTB = "json.data.lttb"

    CFG = "json.cfg"
    CFG_ASK = "".join([CFG, "?"])

    LOG = "str.log"

    def __init__(self, name=""):
        """
        工具构造函数

        Args:
            name: 模块名
        """
        self.name = name

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if pydash.is_empty(value):
            value = "mq-model"
        self.__name = value

    def gen_topic(self, base: str, ex: str = "", name: str = ""):
        """生成话题

        生成符合规则的话题，其模式为
        [传输类型.数据类型].name(.扩展标识)

        Args:
            base: [传输类型.数据类型]，只能选用目前已有的话题类型
            ex: 扩展标识
            name: 模型名称

        Returns:
            符合规则的话题
        """

        # 处理 name 不存在的情况
        if pydash.is_empty(name):
            name = self.__name

        arr = [base, name]
        if not pydash.is_empty(ex):
            arr.append(ex)
        return ".".join(arr)

    def gen_heart_topic(self):
        return self.gen_topic(self.HEART)

    def gen_req_topic(self, ex="", name=""):
        return self.gen_topic(self.REQ, ex, name)

    def gen_res_topic(self, ex="", name=""):
        return self.gen_topic(self.RES, ex, name)

    def gen_data_topic(self, ex="", name=""):
        return self.gen_topic(self.DATA, ex, name)

    def gen_data_raw_topic(self, ex="", name=""):
        return self.gen_topic(self.DATA_RAW, ex, name)

    def gen_data_lttb_topic(self, ex="", name=""):
        return self.gen_topic(self.DATA_LTTB, ex, name)

    def gen_cfg_topic(self, ex="", name=""):
        return self.gen_topic(self.CFG, ex, name)

    def gen_cfg_ask_topic(self, name=""):
        if pydash.is_empty(name):
            name = self.__name

        return self.CFG_ASK + name

    def gen_log_topic(self, ex="", name=""):
        return self.gen_topic(self.LOG, ex, name)


class MQHandlerInitError(Exception):
    pass


class MQLogHandler(logging.Handler):

    def __init__(self, mq, topic):
        logging.Handler.__init__(self)
        self.topic = topic
        self.mq: GRPCMQ = mq
        self.setFormatter(get_formatter())

    def emit(self, record):
        out = self.format(record)
        self.mq.publish(self.topic, out.encode())


def empty_json_message():
    """
    一个空的 json 对象数据
    Returns:

    """
    return json.dumps({}).encode()


class MQBaseModule:
    """
    GRPC MQ 调用的基本模型

    Args:
        cfg (MQModuleCfg): MQHandler 的配置参数

    Attributes:
        cfg (MQModuleCfg):
    """

    def __init__(self, cfg: MQModuleCfg):

        if pydash.is_empty(cfg.name):
            raise MQHandlerInitError("cfg 参数内没有 name")

        # 变量定义
        self.cfg = cfg
        self.topic_tool = MQTopic(cfg.name)
        self.mq: GRPCMQ = GRPCMQ()

        self.log: logging.Logger = get_app_logger("mq-model." + cfg.name)  # 日志数据

        self.app_cfg = None
        self.app_cfg_jsonpath: Optional[JSONUtil] = None

        self.__run = False  # 控制是否在运行状态
        self.__q: IYSQueue = None
        self.__handlers: List[MQModuleHandlerFunc] = []
        self.__packs = {}
        self.__logger_handler = MQLogHandler(self.mq, self.topic_tool.gen_log_topic())
        self.__topics = []

        # 增加 心跳话题处理类
        if self.cfg.useHeartHandler:
            self.add_topic(self.topic_tool.HEART_ASK)
            self.add_handler_func(self.topic_tool.HEART_ASK, self.__heart_handler)

        if self.cfg.useLoggerHandler:
            self.log.addHandler(self.__logger_handler)

        if self.cfg.useConfigCenter:
            self.add_topic(self.topic_tool.gen_cfg_topic("", "__all"))
            self.add_topic(self.topic_tool.gen_cfg_topic())
            self.add_handler_func(self.topic_tool.CFG, self.__cfg_handler)

        self.add_handler_func("ok.sub", self.__ok_sub_handler)

    def __del__(self):
        if self.__run:
            self.stop()

    def add_handler_func(self, topic: str, func: Callable):
        """增加处理函数

        根据 topic 增加处理函数，这里的 topic 使用前缀匹配模式，
        出现相同的 topic 时会覆盖原来的 topic

        Args:
            topic: 话题(前缀匹配模式)
            func: 处理函数
        """

        has = None
        for handler in self.__handlers:
            if handler.topic == topic:
                self.log.warning(f"topic {topic} 已存在")
                has = handler
                break

        if has is not None:
            self.__handlers.remove(has)
        self.__handlers.append(MQModuleHandlerFunc(topic, func))

    def del_handler_func(self, topic: str):
        has = None
        for handler in self.__handlers:
            if handler.topic == topic:
                has = handler
                break

        if has is not None:
            self.__handlers.remove(has)
        else:
            self.log.warning(f"topic {topic} 不存在")

    def add_topic(self, topic: str):
        """增加话题

        增加要订阅的话题，重复的话题不会重复添加

        Args:
            topic: 要添加的话题
        """
        if topic not in self.__topics:
            self.__topics.append(topic)
            self.__do_resub()
        else:
            self.log.warning(f"topic {topic} 已存在")

    def del_topic(self, topic: str):
        """删除话题

        删除要订阅的话题，不存在的话题会被忽略

        Args:
            topic: 要删除的话题

        """
        if topic in self.__topics:
            self.__topics.remove(topic)
            self.__do_resub()
        else:
            self.log.warning(f"topic {topic} 不存在")

    def __do_resub(self):
        """重新订阅

        当模块队列数据存在时，执行取消订阅,
        此时模块会自动重新订阅。

        Returns:

        """
        self.un_sub()

    def __gen_sub_topics(self):
        if self.cfg.hasSubAllTopic:
            return ""
        else:
            return ",".join(self.__topics)

    def run(self):
        """
        运行订阅

        阻塞模式运行的订阅处理函数

        Args:
            topics: 订阅的话题组, 通过 `,` 分隔

        Returns:

        """
        self.__run = True
        while self.__run:
            self.__q = self.mq.subscribe(self.__gen_sub_topics())
            while True:
                try:
                    data = self.__q.get()
                    topic = data.topic
                    message = data.message
                    self.log.debug("获取话题：\"{}\"".format(topic))
                    if self.cfg.useUnPack:
                        topic, message = self.__do_unpack(topic, message)

                    if not pydash.is_empty(topic):
                        threading.Thread(target=self.__handle, args=(topic, message)).start()
                except Exception as e:
                    self.__q = None
                    self.log.info("队列取消")
                    break

    def __handle(self, topic: str, msg: Any):
        """
        处理 MQ 消息

        Args:
            topic: 消息话题
            msg: 消息内容 str 字符串或者是 data 二进制数据
        """
        for handler in self.__handlers:
            if topic.startswith(handler.topic):
                handler.func(topic, msg)

    def __heart_handler(self, _, msg):
        """
        心跳话题
        """
        try:
            data = json.loads(msg)
            if self.cfg.name in data:
                self.mq.publish(topic=self.topic_tool.gen_heart_topic(), msg=empty_json_message())
                if self.cfg.useConfigCenter and self._app_cfg is None:
                    self.mq.publish(topic=self.topic_tool.gen_cfg_ask_topic(), msg=empty_json_message())
        except Exception as e:
            self.log.error(e)

    def __cfg_handler(self, topic: str, msg):
        name = topic.split(".")[-1]
        if name == "__all" or name == self.cfg.name:
            self.log.info("获取配置数据")

            self._app_cfg = json.loads(msg)
            self._app_cfg_jsonpath = JSONUtil(self._app_cfg)
            self.log.debug(self._app_cfg)

    def __ok_sub_handler(self, topic, msg):
        """
        订阅成功后，处理订阅 ID 的 topic

        Args:
            topic: 话题
            msg: 消息数据

        Returns:

        """
        id_ = json.loads(msg)["id"]
        self.log.info("订阅ID:{}".format(id_))
        self.__q.id = id_

    def __do_unpack(self, _topic: str, msg: any):

        pattern = r'(.*)\[([0-9]+)-([0-9]+)]$'
        match_obj = re.match(pattern, _topic)
        if match_obj is None:
            return _topic, msg

        _topic = match_obj.group(1)
        index = match_obj.group(2)
        packs = match_obj.group(3)

        if _topic in self.__packs:
            pack = self.__packs[_topic]
            if pack["time"] < datetime.datetime.now():
                self.log.debug("{} 包超时，删除".format(_topic))
                del self.__packs[_topic]

        if _topic not in self.__packs:
            self.__packs[_topic] = {
                "topic": _topic,
                "time": datetime.datetime.now() + datetime.timedelta(seconds=10),
                "packs": int(packs),
                "msgs": []
            }

        packs = self.__packs[_topic]
        packs["msgs"].append({
            "index": int(index),
            "msg": msg
        })

        if (len(packs["msgs"]) >= packs["packs"]):
            m = b""
            msgs = pydash.sort(packs["msgs"], key=lambda item: item["index"])
            for msg in msgs:
                m += msg["msg"]
            del packs
            return _topic, m

        return "", ""

    def pub(self, topic: str, msg: bytes):
        """
        封装 mq 的 publish 函数

        Args:
            topic: 发布的话题
            msg: 字节数组的消息

        Returns:
            发布的数量
        """
        return self.mq.publish(topic, msg)

    def pub_json(self, topic: str, msg: Any):
        """
        将 msg 转换为 json 字符串发送

        Args:
            topic: 发布的话题
            msg: 消息，应该是一个对象或者数组

        Returns:
            发布的数量
        """
        return self.pub(topic, json.dumps(msg, ensure_ascii=False).encode())

    def pub_json_res(self, topic: str, code: int, message: str = "", data=None):
        """
        将 msg 转换为 json 字符串发送

        Args:
            topic: 发布的话题
            code:  0 为无异常，其他值为存在异常
            message: 在 code 非 0 时，应填充此参数
            data: 在 code 为 0 时，应填充此参数

        Returns:
            发布的数量
        """
        if data is None:
            data = {}
        return self.pub_json(topic, {
            "code": code,
            "message": message,
            "data": data,
        })

    def pub_json_res_ok(self, topic: str, data):
        """
        发送成功返回数据

        Args:
            topic: 发布的话题
            data: 成功的数据

        Returns:
            发布的数量
        """
        return self.pub_json_res(topic, 0, data=data)

    def pub_json_res_error(self, topic: str, code: int, msg: str):
        """
        发送异常返回消息

        Args:
            topic: 发布的话题
            msg: 错误消息

        Returns:
            发布的数量
        """
        self.log.warning("[{}]:{}".format(code, msg))
        return self.pub_json_res(topic, code, message=msg)

    def pub_json_any(self, topic: str, msg: Any):
        """
        将 msg 转换为 json 字符串发送

        Args:
            topic: 发布的话题
            msg: 消息，应该是一个对象或者数组

        Returns:
            发布的数量

        """
        return self.pub_json(topic, msg)

    def un_sub(self):
        if self.__q is not None and self.__q.id >= 0:
            self.mq.unsubscribe(self.__q)

    def stop(self):
        self.log.info("停止 {} 模块".format(self.cfg.name))
        if self.__run:
            self.__run = False
            self.un_sub()
        if self.__q is not None:
            self.__q.__del__()
            self.__q = None


if __name__ == '__main__':
    m = MQBaseModule(MQModuleCfg(name="test"))
    # print(m.get_heart_topic())
